Add icons to model types - #6865
Conversation
Model types can carry a Lucide icon and a colour, so they are recognisable at a glance instead of being told apart by name alone. Both live in public metadata on PageType, under `dashboard-icon-name` and `dashboard-icon-color`. PageTypeUpdateInput carries no metadata, so the icon rides a second call on submit. `createMetadataCreateHandler` could not be reused: it hardcodes `keysToDelete: []`, which would leave "Reset" a silent no-op. `UpdateMetadata` carries both halves in one round trip and merges by key, so clearing works and unrelated metadata is untouched. Icons resolve through `dynamicIconImports`, which keeps all ~1900 icon modules alive. The blanket `node_modules -> "vendor"` chunk rule would then inline every one of them into the main bundle, measured at +757kB minified against origin/main. An explicit chunk per icon keeps them out: vendor grows 20kB gzipped and a rendered icon costs about 700 bytes. The cost is 1651 extra files in dist. The datagrid paints on canvas and cannot await an import, so it draws the fallback glyph and repaints once the visible page's icons land, rather than blocking the list. Lucide ships each icon's raw node data next to the component, so the existing `iconNodeToSvg` and `drawIconLabelCell` cover the cell. Types without an icon render a neutral `Shapes` fallback, which is imported statically and doubles as the placeholder for the frame before a name resolves — the icon slot never renders empty and never reflows. Picker results are capped at 60 rather than virtualised, since every rendered tile fires its own lazy import. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: e3834c1 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
Pull request overview
This PR introduces configurable model type (PageType) icons + colors stored in public metadata (dashboard-icon-name, dashboard-icon-color) and threads them through the Dashboard UI so model types are visually recognizable across lists, tabs, pins, pickers, and reference chips. It also adjusts bundling so Lucide’s dynamically imported icons don’t bloat the main vendor bundle.
Changes:
- Add a
ModelTypeIconsystem (constants, metadata read/write helpers, lazy Lucide loading, icon picker UI, canvas/datagrid rendering). - Extend relevant GraphQL selections/fragments/fixtures to include
PageType.metadatawhere needed for icon resolution. - Render the chosen icon in key UX surfaces (model type list/details, model list tabs + datagrid “Type” column, navigation pins + manager, model reference chips).
Reviewed changes
Copilot reviewed 60 out of 60 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| vite.config.js | Split Lucide icon modules into per-icon chunks to keep them out of the main vendor bundle. |
| src/searches/usePageTypeSearch.ts | Include metadata in page type search results for icon resolution. |
| src/searches/usePageSearch.ts | Include pageType.metadata in page search results for icon resolution. |
| src/navigationPins/types.ts | Add icon to resolved navigation pin type. |
| src/navigationPins/injectNavigationPins.tsx | Render pin icon in sidebar menu items. |
| src/navigationPins/injectNavigationPins.test.ts | Update pin test factory to include icon. |
| src/navigationPins/hooks/useResolvedNavigationPins.ts | Resolve icons for pins with fallback. |
| src/navigationPins/hooks/usePinnedModelTypeNames.ts | Return { names, icons } lookup from pinned model types. |
| src/navigationPins/fragments/pinnedModelType.ts | Add metadata { key value } to pinned model type fragment. |
| src/navigationPins/components/PinModelTypeDialog.tsx | Show model type icon in the pin dialog (when available). |
| src/navigationPins/components/NavigationPinList.tsx | Show icons in the pin manager list. |
| src/navigationPins/components/NavigationPinButton.tsx | Pass selected model type icon into the pin flow. |
| src/navigationPins/buildPinnedModelTypesDocument.test.ts | Snapshot update for new fragment fields. |
| src/modelTypes/views/PageTypeDetails.tsx | Persist icon via metadata update after page type update. |
| src/modelTypes/utils/pageTypePageForm.ts | Treat icon changes as making the form “dirty”. |
| src/modelTypes/utils/pageTypePageForm.test.ts | Update test data for new icon field. |
| src/modelTypes/fixtures.ts | Add metadata: [] in page type fixtures. |
| src/modelTypes/components/PageTypeList/PageTypeList.tsx | Render model type icon in the page type list rows. |
| src/modelTypes/components/PageTypeDetailsPage/Title.tsx | Render model type icon in the details title/header. |
| src/modelTypes/components/PageTypeDetailsPage/PageTypeDetailsPage.tsx | Add icon to form state and wire icon changes to Form.set. |
| src/modelTypes/components/PageTypeDetails/PageTypeDetails.tsx | Add ModelTypeIconPicker next to the name field. |
| src/modeling/views/PageList/PageList.tsx | Use icon-bearing options for the page type picker dialog. |
| src/modeling/queries.ts | Include metadata in pageTypeQuery selection. |
| src/modeling/fixtures.ts | Add metadata: [] in modeling fixtures. |
| src/modeling/components/PageListPage/PageListPage.tsx | Compute active type icon and pass to NavigationPinButton. |
| src/modeling/components/PageListDatagrid/PageListDatagrid.tsx | Preload Lucide icons and repaint datagrid when cached. |
| src/modeling/components/PageListDatagrid/datagrid.ts | Render model type cell with icon in the “Type” column. |
| src/modeling/components/ModelTypeTabs/ModelTypeTabs.tsx | Render icons in type/group tabs where applicable. |
| src/graphql/types.generated.ts | Regenerate types to reflect new metadata selections. |
| src/graphql/hooks.generated.ts | Regenerate hooks/documents to include MetadataItem usage. |
| src/fragments/pageTypes.ts | Add metadata { ...MetadataItem } to PageType fragment. |
| src/fragments/pages.ts | Add pageType.metadata to Page fragment. |
| src/components/SortableChipsField/SortableChipsField.tsx | Carry per-chip model type icon for sortable reference chips. |
| src/components/SortableChip/SortableChip.tsx | Add startAdornment slot to sortable chips. |
| src/components/ModelTypeIcon/usePreloadedLucideIcons.ts | Hook to preload icons and trigger repaint for canvas consumers. |
| src/components/ModelTypeIcon/toModelTypeOption.tsx | Map model type nodes to combobox options with icon adornment. |
| src/components/ModelTypeIcon/renderModelTypeIconSvg.ts | Render model type icon SVG for canvas usage. |
| src/components/ModelTypeIcon/ModelTypeIconPicker.tsx | UI for searching/selecting icon + color (and resetting). |
| src/components/ModelTypeIcon/ModelTypeIconPicker.stories.tsx | Storybook coverage for icon picker. |
| src/components/ModelTypeIcon/ModelTypeIconPicker.module.css | Styles for the icon picker grid/swatches/trigger. |
| src/components/ModelTypeIcon/ModelTypeIcon.tsx | Lazy-load Lucide icon component with stable fallback glyph. |
| src/components/ModelTypeIcon/ModelTypeIcon.stories.tsx | Storybook coverage for icon rendering and edge cases. |
| src/components/ModelTypeIcon/messages.ts | i18n messages for icon picker UI. |
| src/components/ModelTypeIcon/loadLucideIcon.ts | Lazy icon import map + caching + preload helpers. |
| src/components/ModelTypeIcon/getModelTypeIcon.ts | Read/write icon metadata helpers + fallback + equality helper. |
| src/components/ModelTypeIcon/getModelTypeIcon.test.ts | Unit tests for metadata read/write/equality behavior. |
| src/components/ModelTypeIcon/constants.ts | Define metadata keys, icon colors, and color resolution. |
| src/components/Datagrid/customCells/useCustomCellRenderers.ts | Register model type custom cell renderer. |
| src/components/Datagrid/customCells/ModelTypeCell.tsx | Canvas cell implementation for icon+label model type cells. |
| src/components/ChipField/ModelTypeChipIcon.tsx | Render model type icon in reference chips when present. |
| src/components/ChipField/ChipField.tsx | Add startAdornment slot to chips. |
| src/components/Attributes/utils.ts | Thread model type icon through reference display value helpers. |
| src/components/Attributes/SingleReferenceField.tsx | Show model type icon in selected single-reference chip. |
| src/components/AssignModelDialog/AssignModelDialog.test.tsx | Update mocks to include pageType.metadata. |
| src/attributes/utils/getAssignedModelTypesForAttribute.test.ts | Update fixtures with metadata: []. |
| src/attributes/utils/data.ts | Attach model type icon to page references. |
| src/attributes/utils/data.test.ts | Assert reference display includes fallback icon. |
| src/attributes/utils/computeTypeTabCounts.test.ts | Update fixtures with metadata: []. |
| locale/defaultMessages.json | Add localized strings for the icon picker UI. |
| .changeset/model-type-icons.md | Changeset describing the feature and metadata storage keys. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| export const ModelTypeIcon = ({ | ||
| icon, | ||
| size = iconSize.small, | ||
| strokeWidth = iconStrokeWidthBySize.small, | ||
| className, | ||
| }: ModelTypeIconProps) => { |
…e-icons # Conflicts: # src/graphql/hooks.generated.ts # src/modeling/components/PageListPage/PageListPage.tsx
The picker was switched to the shared `buttonMessages.reset` after messages were last extracted, leaving its own "Reset" entry orphaned in defaultMessages.json. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #6865 +/- ##
==========================================
+ Coverage 58.09% 58.17% +0.07%
==========================================
Files 3374 3388 +14
Lines 73157 73440 +283
Branches 19337 19392 +55
==========================================
+ Hits 42500 42722 +222
- Misses 28785 28842 +57
- Partials 1872 1876 +4
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
| const iconsById = useMemo( | ||
| () => | ||
| (pageTypes ?? []).reduce<Record<string, ModelTypeIconValue>>( | ||
| (acc, pageType) => ({ ...acc, [pageType.id]: getModelTypeIcon(pageType.metadata) }), | ||
| {}, | ||
| ), | ||
| [pageTypes], | ||
| ); |
| export const loadLucideIcon = (name: string): Promise<LoadedLucideIcon | null> => { | ||
| const cached = cache.get(name); | ||
|
|
||
| if (cached) { | ||
| return Promise.resolve(cached); | ||
| } | ||
|
|
||
| const inFlight = pending.get(name); | ||
|
|
||
| if (inFlight) { | ||
| return inFlight; | ||
| } |
…e-icons # Conflicts: # src/attributes/utils/data.ts # src/modelTypes/components/PageTypeList/PageTypeList.tsx # src/modelTypes/views/PageTypeDetails.tsx # src/modeling/components/ModelTypeTabs/ModelTypeTabs.tsx # src/modeling/components/PageListDatagrid/PageListDatagrid.tsx # src/modeling/views/PageList/PageList.tsx # src/navigationPins/components/NavigationPinButton.tsx # src/navigationPins/components/NavigationPinList.tsx # src/navigationPins/hooks/usePinnedModelTypeNames.ts # src/navigationPins/hooks/useResolvedNavigationPins.ts
c84b521 to
9eec53d
Compare
…r trigger Reference rows in the assign dialog listed models by name alone, so the icon that identifies a model type everywhere else stopped at the dialog boundary. `SearchPages` already selects the type's metadata, so no extra fetch is needed. The adornment is a render prop on the dialog rather than a field on `Container`: that shape doubles as the submit payload, and putting React nodes into submitted data would leak presentation into every assign handler. Rows resolve the icon per row instead. The picker trigger is now an explicit square. `aspect-ratio` cannot do this here — as a flex item its width resolves from content before `align-self: stretch` makes the height definite, so the ratio had nothing to compute from and it rendered 34x52 against the input's 52. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reference chips resolved their label through one of four branches, and only the reference-search branch carried the model type icon. Searches stay empty until the assign dialog runs, so a saved reference always fell through to an icon-less branch and rendered bare. Look the icons up by id instead, so they no longer depend on which label branch won: useModelReferenceIcons reuses the existing SearchPages query (which already selects pageType.metadata) filtered to the assigned ids, and the resulting map takes precedence in both reference display utils. Also spaces the chip's adornment: SortableChip now uses a single gap on its row rather than per-child margins, so the icon gets the same 4px as the drag handle and an absent icon adds no stray gap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Model types are told apart by name alone today, which gets slow to scan once a shop has more than a handful. This gives each one an optional Lucide icon and a colour, stored in public metadata on
PageType(dashboard-icon-name,dashboard-icon-color), and renders it everywhere a model type is listed or referenced.Screenshots
<canvas>rather than rendering React.What it does
Reset, which deletes both metadata keys rather than blanking them, so the metadata editor stays free of dead entries.Shapesglyph everywhere. The slot is a fixed width, so configuring one doesn't reflow the list around it.Notes for review
PageTypeUpdateInputcarries no metadata, so the icon rides a second call on submit.createMetadataCreateHandlercouldn't be reused: it hardcodeskeysToDelete: [], which would have leftReseta silent no-op.UpdateMetadatacarries both halves in one round trip and merges by key, so clearing works and unrelated metadata is untouched.There is a build-config change in
vite.config.jsand it is load-bearing. Icons resolve throughdynamicIconImports, which keeps all ~1900 icon modules alive. The blanketnode_modules → "vendor"rule would then inline every one of them into the main bundle. Measured againstorigin/main:origin/mainSo the naive version costs +757kB minified on every page load. An explicit chunk per icon keeps them out: vendor grows 20kB gzipped (+1.4%) and a rendered icon costs ~700 bytes, fetched once and cached. The trade-offs are 1,651 extra files in
dist, and the ~34 icons the entry chunk uses statically becoming small separate requests instead of living inside vendor.The datagrid can't await. Canvas draws synchronously, so it paints the fallback glyph and repaints once the visible page's icons resolve, rather than blocking the list on a set of lazy imports.
ChipField/SortableChipgained astartAdornmentprop because theirlabelintersects with an HTML attribute onBoxProps, typing itReactNode & string— an element can't be passed through it.